package net.borderwars.convert;

import org.apache.bcel.Constants;
import org.apache.bcel.generic.*;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;

/**
 * @author ehubbard
 *         Date: Mar 21, 2005
 *         Time: 3:05:22 PM
 */
public abstract class CRobotOpCode {

    protected int lineNumber = 0;
    protected String opCode = null;
    protected ArrayList<String> args = new ArrayList<String> ();
    static HashMap<Integer, LinkedList<BranchHandle>> branches = new HashMap<Integer, LinkedList<BranchHandle>> ();
    private static Set<Integer> fields = new TreeSet<Integer> ();

    abstract public String getName ();

    abstract public InstructionList getJavaInstructions ();

    static HashMap opCodeTable = new HashMap ();
    static Class setDataArgs[] = {Integer.TYPE, String.class, ArrayList.class};

    public static InstructionList getInstructions (String line, ClassGen cg, CRobotMethod crm, MethodGen mg) {
        int lineNumber = 0;
        String opCode = null;
        ArrayList<String> args = new ArrayList<String> ();
        line = line.trim ();
        int a = line.indexOf (":");
        if (a < 0) {
            System.out.println ("line " + line);
            System.exit (1);
        }
        int spaceBeforeArgs = line.trim ().indexOf (" ", a + 2);
        if (spaceBeforeArgs != -1) { // if its -1 then there are no args for this instruction!
            StringTokenizer stArgs = new StringTokenizer (line.substring (spaceBeforeArgs), ",");
            while (stArgs.hasMoreTokens ()) {
                args.add (stArgs.nextToken ().trim ());
            }
        }

        lineNumber = Integer.parseInt (line.substring (0, a - 1));
        if (spaceBeforeArgs < 0) {
            spaceBeforeArgs = line.length ();
        } // if no args.. then marker is at end
        opCode = line.substring (a + 1, spaceBeforeArgs).trim ();
        try {
            return (getInstructions (lineNumber, opCode, args, cg, crm, mg));
        }
        catch (Exception e) {
            System.out.println ("Failed on this: " + opCode + " " + args);
            e.printStackTrace ();
            System.exit (1);
        }
        return (null);

    }

    private static int getMethodNumber (String arg) {
        return (Integer.parseInt (arg));
    }

    private static int getIndex (String arg) {

        int idx = arg.indexOf (" ");
        String narg = arg.substring (0, idx);
        int lvn = Integer.parseInt (narg);
        return (lvn);
    }

    private static boolean isExternal (String arg) {
        int idx = arg.indexOf ("external");
        if (idx == -1) {
            return (false);
        }
        return (true);
    }

    static HashSet genericOps = new HashSet ();

    static {
        genericOps.add ("*");
        genericOps.add ("-");
        genericOps.add ("/");
        genericOps.add ("-=");
        genericOps.add ("+=");
        genericOps.add ("<");
        genericOps.add ("<=");
        genericOps.add (">");
        genericOps.add ("+");
        genericOps.add ("%");
        genericOps.add ("&&");
        genericOps.add ("%=");
        genericOps.add ("*=");
        genericOps.add ("/=");
        genericOps.add ("||");
        genericOps.add ("^");
        genericOps.add ("^=");
        genericOps.add (">>");
        genericOps.add (">=");
        genericOps.add ("<<");
        genericOps.add (">>=");
        genericOps.add ("&");

    }

    private static boolean isGeneric (String cmd) {
        return (genericOps.contains (cmd));
    }

    static HashMap<Integer, InstructionHandle> handles = new HashMap<Integer, InstructionHandle> ();

    static void resetState () {
        handles = new HashMap<Integer, InstructionHandle> ();
        localVariables = new HashMap<String, LocalVariableGen> ();
        fields = new TreeSet<Integer> ();
    }

    static MethodGen mg = null;
    static ClassGen cg = null;

    static HashMap<String, LocalVariableGen> localVariables = new HashMap<String, LocalVariableGen> ();

    public static InstructionList getInstructions (int ln, String opCode, ArrayList<String> args, ClassGen cg, CRobotMethod crm, MethodGen mg) {
        CRobotOpCode.mg = mg;
        CRobotOpCode.cg = cg;
        InstructionList rtn = null;
        InstructionFactory fac = new InstructionFactory (cg);

        // System.out.println(ln + " " + opCode + " " + args);
        if (opCode.equalsIgnoreCase ("fetch")) {
            rtn = new InstructionList ();
            String firstArg = args.get (0).trim ();
            boolean external = isExternal (firstArg);
            int idx = getIndex (firstArg);
            if (external) {
                String name = CRobotMethod.externalSymbolTable.get (idx);
                rtn.append (fac.createFieldAccess (cg.getClassName (), name, Type.INT, Constants.GETFIELD));
//                FieldGen fg = new FieldGen(Constants.ACC_PUBLIC, Type.INT, name, cg.getConstantPool());
//                Field f = fg.getField();
//                cg.addField(f);
//                rtn.append(new GETFIELD(f.getNameIndex()));
                //  throw new UnsupportedOperationException("eeek we didn't find hte field " + name + " within " + fs);

            }
            else {
                String name = CRobotMethod.localSymbolTable.get (idx);
                LocalVariableGen lvg = localVariables.get (name);
                if (lvg == null) {
                    //System.err.println("Adding " + name);
                    lvg = mg.addLocalVariable (name, Type.INT, null, null);
                    localVariables.put (name, lvg);
                }
                rtn.append (new ALOAD (lvg.getIndex ()));

            }
        }
        else
            if (opCode.equalsIgnoreCase ("store") || opCode.equalsIgnoreCase ("binop")) {
                boolean isStore = opCode.equalsIgnoreCase ("store");
                //  local variable, rightside
                rtn = new InstructionList ();

                String cmd = null;
                if (isStore) {
                    cmd = args.get (1);
                }
                else {
                    cmd = args.get (0);
                }
                if (cmd.charAt (0) == '(') // if the compiler put () around the command.. just trim it out..
                    // I hope it has no special meaning
                    cmd = cmd.substring (1, 2);

                if (cmd.equals ("=")) {
                    rtn.append (new SWAP ());
                    rtn.append (new POP ());

                    if (isStore) {
                        int lvn = getIndex (args.get (0));

                        rtn.append (new DUP ());
                        rtn.append (new ASTORE (lvn));
                    }

                }
                else
                    if (isGeneric (cmd)) {
                        genericBinaryOp (args, rtn, cmd, isStore);

                    }
                    else
                        if (cmd.equals ("!")) {
                            BranchHandle bh = rtn.append (new IF_ICMPNE (null));
                            rtn.append (createConstant (1));
                            BranchHandle gh = rtn.append (new GOTO (null));
                            InstructionHandle ih = rtn.append (new NOP ());
                            updateBranchHandle (bh, null, ih);
                            rtn.append (createConstant (0));
                            ih = rtn.append (new NOP ());
                            updateBranchHandle (gh, null, ih);
                        }
                        else
                            if (cmd.equals ("!=")) {
                                if (!isStore) {
                                    BranchHandle bh = rtn.append (new IF_ICMPEQ (null));
                                    rtn.append (new POP ());
                                    rtn.append (createConstant (1));
                                    BranchHandle gh = rtn.append (new GOTO (null));
                                    InstructionHandle ih = rtn.append (new NOP ());
                                    updateBranchHandle (bh, null, ih);
                                    ih = rtn.append (new NOP ());
                                    updateBranchHandle (gh, null, ih);

                                }
                                else {
                                    throw new UnsupportedOperationException ("We don't support " + cmd + " yet");
                                }
                            }
                            else
                                if (cmd.equals ("==")) {
                                    if (!isStore) {
                                        BranchHandle bh = rtn.append (new IF_ICMPNE (null));
                                        rtn.append (new POP ());
                                        rtn.append (createConstant (1));
                                        BranchHandle gh = rtn.append (new GOTO (null));
                                        InstructionHandle ih = rtn.append (new NOP ());
                                        updateBranchHandle (bh, null, ih);
                                        ih = rtn.append (new NOP ());
                                        updateBranchHandle (gh, null, ih);

                                    }
                                    else {
                                        System.out.println ("eegads we don't do this!");
                                    }
                                }
                                else {
                                    throw new UnsupportedOperationException ("We don't support " + cmd + " yet");
                                }

            }
            else
                if (opCode.equalsIgnoreCase ("const")) {
                    String s = args.get (0);
                    int c = Integer.parseInt (s);
                    rtn = new InstructionList (createConstant (c));
                }
                else
                    if (opCode.equalsIgnoreCase ("fcall")) {
                        rtn = new InstructionList ();
                        int lvn = getMethodNumber (args.get (0));
                        rtn.append (new INVOKEVIRTUAL (lvn));

                    }
                    else
                        if (opCode.equalsIgnoreCase ("retsub")) {
                            rtn = new InstructionList ();
                            rtn.append (InstructionFactory.createReturn (Type.INT));

                        }
                        else
                            if (opCode.equalsIgnoreCase ("branch")) {
                                rtn = new InstructionList (createConstant (0));
                                BranchHandle bh = rtn.append (new IF_ICMPEQ (null));
                                int gotoThisLine = Integer.parseInt (args.get (0));
                                // System.out.println("goto this line " + gotoThisLine);

                                if (gotoThisLine < ln) {
                                    //   System.out.println("cool already knew about this line");
                                    updateBranchHandle (bh, null, handles.get (gotoThisLine));
                                }
                                else {

                                    LinkedList<BranchHandle> ll = branches.get (gotoThisLine);
                                    if (ll == null) {
                                        ll = new LinkedList<BranchHandle> ();
                                        branches.put (gotoThisLine, ll);
                                    }
                                    ll.add (bh); //  set aside a lookup so later we can update it

                                }
                            }
                            else
                                if (opCode.equalsIgnoreCase ("chop")) {
                                    rtn = new InstructionList ();
                                    rtn.append (new POP ());
                                }
                                else
                                    if (opCode.equalsIgnoreCase ("frame")) {
                                        /**
                                         * I think I can ignore this in java... but we'll see
                                         */
                                        rtn = new InstructionList ();
                                        rtn.append (new NOP ());
                                    }
                                    else
                                        if (opCode.equalsIgnoreCase ("nop")) {
                                            rtn.append (new NOP ());
                                        }
        if (branches.containsKey (ln)) { // oo then somewhere someone pointed to this instruction!
            InstructionHandle start = rtn.getStart ();
            LinkedList<BranchHandle> ll = branches.get (ln);
            for (BranchHandle bh : ll) {
                updateBranchHandle (bh, null, start);
                branches.remove (ln);
                // System.out.println("Updating a branch handle " + ln);
            }
        }

        handles.put (ln, rtn.getStart ());

        return (rtn);
    }

    /**
     * Uses PUSH to push a constant value onto the stack.
     *
     * @param value must be of type Number, Boolean, Character or String
     */
    static public Instruction createConstant (Object value) {
        PUSH push;
        ConstantPoolGen cp = cg.getConstantPool ();

        if (value instanceof Number)
            push = new PUSH (cp, (Number) value);
        else
            if (value instanceof String)
                push = new PUSH (cp, (String) value);
            else
                if (value instanceof Boolean)
                    push = new PUSH (cp, (Boolean) value);
                else
                    if (value instanceof Character)
                        push = new PUSH (cp, (Character) value);
                    else
                        throw new ClassGenException ("Illegal type: " + value.getClass ());

        return push.getInstruction ();
    }

    private static void updateBranchHandle (BranchHandle bh, InstructionHandle old, InstructionHandle in) {
        assert in != null;
        bh.updateTarget (old, in);
    }

    private static void genericBinaryOp (ArrayList<String> args, InstructionList rtn, String cmd, boolean store) {
        rtn.append (InstructionFactory.createBinaryOperation (cmd, Type.INT));
        if (store) {
            int lvn = getIndex (args.get (0));
            rtn.append (new DUP ());
            rtn.append (new ASTORE (lvn));

        }
    }

    static {

        /**
         * FETCH offset (external | local) - Fetch will retrieve a word from either the external variable pool
         * or the local variable pool and push it onto the stack. The offset has its high-bit set (or'ed with 0x8000)
         * if it is an external (offset from the beginning of the stack), otherwise it is a local (offset from the local
         * variable mark). See STORE.
         */
        opCodeTable.put ("fetch", InstructionFactory.ALOAD_0);

        /**
         * STORE offset (external | local), opcode - Store pops the top two items, applies the arithmetic opcode to the
         * two operands, pushes the result on the top of the stack and stores it in the variable referenced by the
         * offset. Offsets are either external or local, according to the method described in Fetch. The result of the
         * opcode is left on the stack. See FETCH and BINOP.
         */
        opCodeTable.put ("store", InstructionFactory.ASTORE_0);

        /**
         *    CONST k - Const will push a constant onto the stack.
         */
        opCodeTable.put ("const", InstructionFactory.ICONST_0); // pushes a constant onto the estack

        /**
         * BINOP opcode - Binop will pop the top two items as top of stack = y, next to top of stack as x, apply the
         * arithmetic opcode as (x opcode y), and push the result on the stack. Opcodes are decimal representations of
         * 'C' operators such as '+', '/', '>=', etc. See STORE.
         */
        opCodeTable.put ("binop", InstructionFactory.IADD); // pop two items, do match push result back on stack

        /**
         *  FCALL link-offset - Fcall performs a high level function call facility. The link-offset operand specifies
         * an entry in the link list table. Fcall pushes its return information: the next instruction counter and the
         * current local variable mark. A new local variable mark and temporary mark (top of stack pointer) is set.
         * The cpu then branches to the first instruction of the function. See RETSUB and FRAME.
         */
        opCodeTable.put ("fcall", InstructionFactory.ATHROW);

        /**
         * RETSUB - Retsub returns from a function, leaving the return value on the top of the stack. Retsub restores
         * the previous local variable pool, the next instruction counter, and re-adjusts the stack frame to the point
         * just before the call. The C compiler generates code to return a dummy value if the function does not
         * explicitly return one. See FCALL and FRAME.
         */

        /**
         *  BRANCH instruction - Branch pops the top of the stack and branches to the instruction if the value is zero.
         * The next sequential instruction is executed if the value is anything other than zero.
         */

        /**
         * CHOP - Chop discards the top of the stack by popping it into obilvion.
         */

        /**
         * FRAME - Frame facilitates fcall/retsub by saving the current top of stack pointer (temporary mark) in
         * anticipation of a fcall statement. The top of stack pointer is saved in the call/return stack as a frame
         * marker. See FCALL and RETSUB.
         */

        /**
         * NOP - No operation. Is used as a mark indicating the end of code.
         */

    }

    public static CRobotOpCode getCRobotOpCode (String line) {
        int lineNumber = 0;
        String opCode = null;
        ArrayList<String> args = new ArrayList<String> ();

        line = line.trim ();
        int a = line.indexOf (":");
        int spaceBeforeArgs = line.substring (a + 2).trim ().indexOf (" ");

        StringTokenizer stArgs = new StringTokenizer (line.substring (spaceBeforeArgs), ",");

        while (stArgs.hasMoreTokens ()) {
            args.add (stArgs.nextToken ());
        }

        lineNumber = Integer.parseInt (line.substring (0, a - 1));
        opCode = line.substring (a + 1, spaceBeforeArgs).trim ();

        Class c = (Class) opCodeTable.get (opCode);
        Method m = null;
        try {
            m = c.getMethod ("setData", setDataArgs);
            CRobotOpCode coc = (CRobotOpCode) c.newInstance ();
            Object oargs[] = new Object[3];
            oargs[0] = new Integer (lineNumber);
            oargs[1] = opCode;
            oargs[2] = args;
            m.invoke (coc, oargs);
            return (coc);
        }
        catch (NoSuchMethodException e) {
            e.printStackTrace ();
            System.exit (1);
        }
        catch (IllegalAccessException e) {
            e.printStackTrace ();
            System.exit (1);
        }
        catch (InvocationTargetException e) {
            e.printStackTrace ();
            System.exit (1);
        }
        catch (InstantiationException e) {
            e.printStackTrace ();
            System.exit (1);
        }
        return (null);
    }

    public void setData (int ln, String opCode, ArrayList<String> args) {
        this.lineNumber = ln;
        this.opCode = opCode;
        this.args = args;
    }

    public static Set<Integer> getFields () {
        return (fields);
    }
}
